@tool
extends Control

@export var dialog_data: Resource = preload("res://addons/madtalk/runtime/madtalk_data.tres")
var current_sheet = null

# Scene templates
var DialogNode_template = preload("res://addons/madtalk/components/DialogNode.tscn")
var SideBar_SheetItem_template = preload("res://addons/madtalk/components/SideBar_SheetItem.tscn")

# Scene nodes
@onready var graph_area = get_node("GraphArea")


@onready var sidebar_sheetlist = get_node("SideBar/Content/SheetsScroll/VBox")
@onready var sidebar_current_panel = get_node("SideBar/Content/CurrentPanel")
@onready var SideBar_sheet_id = get_node("SideBar/Content/CurrentPanel/SheetIDLabel")
@onready var SideBar_sheet_desc = get_node("SideBar/Content/CurrentPanel/DescEdit")
@onready var SideBar_search = get_node("SideBar/Content/SearchEdit")

@onready var graph_area_popup = get_node("PopupMenu")

@onready var popup_delete_node = get_node("DialogDeleteNodePopup")

@onready var dialog_sheet_edit = get_node("DialogSheetEdit")
@onready var dialog_sheet_rename_error_popup = get_node("DialogSheetRenameError")
@onready var dialog_sheet_create_popup = get_node("DialogSheetCreated")
@onready var dialog_export = get_node("DialogExport")
@onready var dialog_import = get_node("DialogImport")

# Maps sequence ids to graph nodes
var sequence_map: Dictionary = {}

# Holds the node being deleted when user presses X
var deleting_node = null

# Holds the item object being dragged
var dragging_object = null
var hovering_object = null


func _ready() -> void:
	pass
	#call_deferred("setup")

func setup():
	if dialog_data.sheets.size() == 0:
		create_new_sheet()
	
	else:
		open_sheet(dialog_data.sheets.keys()[0])
	
	dialog_export.setup(dialog_data, current_sheet)
	dialog_import.setup(dialog_data, current_sheet)
	
# Opens a sheet for the first time, or reopens (updates area content)
func open_sheet(sheet_id: String) -> void:
	# FAILSAFE: We ignore this call if the sheet id is invalid
	if not sheet_id in dialog_data.sheets:
		print("Invalid sheet id \"%s\"" % sheet_id)
		return
		
	# Clear all current content in the graph area
	for dialog_node in graph_area.get_children():
		if dialog_node is DialogGraphNode:
			graph_area.remove_child(dialog_node)
			dialog_node.queue_free()
			
	sequence_map.clear()
	
	# Prepare new sheet
	current_sheet = sheet_id
	var sheet_data = dialog_data.sheets[sheet_id]
	
	# First we build all nodes *without* updating them
	for node_data in sheet_data.nodes:
		var new_node = create_node_instance(node_data, false)
		sequence_map[node_data.sequence_id] = new_node
	
	# After we have all node instances available for connections,
	# we update them 
	for sequence_id in sequence_map:
		sequence_map[sequence_id].update_from_data()
			
	graph_area.scroll_offset.y -= 1
	update_sidebar()
	rebuild_connections()
	
	await get_tree().process_frame
	graph_area.scroll_offset.y += 1
	graph_area.queue_redraw()

func reopen_current_sheet():
	open_sheet(current_sheet)

# Creates the visual representation of a node
# Does not modify the data structure
func create_node_instance(node_data: Resource, update_now: bool = true) -> DialogGraphNode:
	var new_node: GraphNode = DialogNode_template.instantiate()
	new_node.name = "DialogNode_ID%d" % node_data.sequence_id
	new_node.main_editor = self
	graph_area.add_child(new_node)
	new_node.position_offset = node_data.position
	new_node.connections_changed.connect(_on_node_connections_changed)
	new_node.mouse_entered.connect(_on_sequence_mouse_entered.bind(new_node))
	new_node.mouse_exited.connect(_on_sequence_mouse_exited.bind(new_node))
	#new_node.connect("close_request", Callable(self, "_on_node_close_request").bind(new_node))
	new_node.data = node_data 	# Assign the reference, not a copy
								# Any changes to this node will reflect back in
								# the main Resource
	#new_node.show_close = (node_data.sequence_id != 0)
	if (node_data.sequence_id != 0):
		var new_close_btn = Button.new()
		new_close_btn.text = " X "
		new_close_btn.focus_mode = Control.FOCUS_NONE
		new_node.get_titlebar_hbox().add_child(new_close_btn)
		new_close_btn.pressed.connect(_on_node_close_request.bind(new_node))
	
	# During sheet building not all nodes are ready so updating connections
	# will fail. In such a case we skip this task and update all nodes at once
	# later
	if (update_now):
		new_node.update_from_data()
	
	return new_node
	
# Creates a new node, optionally creating the visual GraphNode
func create_new_node(graph_position: Vector2 = Vector2(0,0), create_visual_instance = false) -> DialogNodeData:
	if not current_sheet:
		return null
		
	var sheet_data = dialog_data.sheets[current_sheet]
	
	# Find next available sequence id
	var next_available_id = sheet_data.next_sequence_id
	for this_node in sheet_data.nodes:
		if this_node.sequence_id >= next_available_id:
			next_available_id = this_node.sequence_id+1

	var new_data = DialogNodeData.new()
	new_data.resource_scene_unique_id = Resource.generate_scene_unique_id()
	new_data.position = graph_position
	new_data.sequence_id = next_available_id
	new_data.items = [] # New Array to avoid sharing references
	new_data.options = [] # New Array to avoid sharing references
	
	sheet_data.nodes.append(new_data)
	sheet_data.next_sequence_id = next_available_id+1
	
	# create_visual_instance is true when the node is created from user
	# interaction ("New sequence" button). It is false when the data is being
	# created procedurally and instances will be created later by open_sheet()
	#     DEPRECATED: now all methods call here with create_visual_instance=false
	#     and call open_sheet() afterwards
	if create_visual_instance:
		create_node_instance(new_data, true)
		rebuild_connections() # Should not be needed but reduntant calls are harmless
	
	return new_data

# Creates a new sheet, set as current, and returns the name of the sheet
func create_new_sheet() -> String:
	# Find a suitable available name
	var sheet_num = 1
	var new_sheet_name = "new_sheet_1"
	while new_sheet_name in dialog_data.sheets:
		sheet_num += 1
		new_sheet_name = "new_sheet_%d" % sheet_num
		
	# Create the new sheet
	var new_sheet_data = DialogSheetData.new() # default next_sequence_id=0
	new_sheet_data.resource_scene_unique_id = Resource.generate_scene_unique_id()
	new_sheet_data.sheet_id = new_sheet_name
	new_sheet_data.nodes = [] # Forces a new array to avoid reference sharing
	dialog_data.sheets[new_sheet_name] = new_sheet_data
	current_sheet = new_sheet_name
	
	# All sheets need at least one node with ID=0
	# Create a node data item without creating the GraphNode instance, as
	# it will be created later by open_sheet()
	create_new_node(Vector2(0,0), false)
	
	# Update sidebar and open sheet
	update_sidebar()
	open_sheet(new_sheet_name)
	#rebuild_connections()
	
	return new_sheet_name
	

# Connections are not build directly from UI
# Instead they are rebuilt from the Resource data objects every time
# This is the safest way to make sure there is never any difference between
# the visual representation and the underlying data
func rebuild_connections() -> void:
	
	graph_area.clear_connections()
	
	for sequence_id in sequence_map:
		var dialog_node = sequence_map[sequence_id]
			
			
		var sequence_data = dialog_node.data
		
		# For each item in this sequence
		for item_data in sequence_data.items:
			# Do we have a connection?
			if (item_data.connected_to_id > -1) and (item_data.port_index > -1):
				var target_node = get_dialognode_by_id(item_data.connected_to_id)
				if target_node:
					graph_area.connect_node(dialog_node.name, item_data.port_index, target_node.name, 0)
					
		# For each option in this sequence
		for opt_data in sequence_data.options:
			# Do we have a connection?
			if (opt_data.connected_to_id > -1) and (opt_data.port_index > -1):
				var target_node = get_dialognode_by_id(opt_data.connected_to_id)
				if target_node:
					graph_area.connect_node(dialog_node.name, opt_data.port_index, target_node.name, 0)
		
		# If we have a continue option at the end
		if sequence_data.continue_sequence_id > -1:
			var target_node = get_dialognode_by_id(sequence_data.continue_sequence_id)
			if target_node:
				graph_area.connect_node(dialog_node.name, sequence_data.continue_port_index, target_node.name, 0)
		

	

# Given a sequence id, returns the corresponding GraphNode object
func get_dialognode_by_id(id: int) -> DialogGraphNode:
	if not id in sequence_map:
		print("Error: node ID %s not found in sequence map" % id)
		return null
		
	return sequence_map[id]
	
func update_sidebar():
	# === Update current sheet
	if current_sheet:
		var sheet_data = dialog_data.sheets[current_sheet]
		SideBar_sheet_id.text = sheet_data.sheet_id
		SideBar_sheet_desc.text = sheet_data.sheet_description
		sidebar_current_panel.show()
		
	else:
		sidebar_current_panel.hide()
	
	# === Update list
	
	# Remove old items
	for old_item in sidebar_sheetlist.get_children():
		sidebar_sheetlist.remove_child(old_item)
		old_item.queue_free()
	
	# Add new items
	var search_term = SideBar_search.text
	for this_sheet_id in dialog_data.sheets:
		var new_item_data = dialog_data.sheets[this_sheet_id]
		# If there is no search, or search shows up in either id or description:
		if (search_term == "") or (search_term in this_sheet_id) or (search_term in new_item_data.sheet_description):
			var new_item = SideBar_SheetItem_template.instantiate()
			sidebar_sheetlist.add_child(new_item)
			new_item.get_node("Panel/SheetLabel").text = new_item_data.sheet_id
			new_item.get_node("Panel/DescriptionLabel").text = new_item_data.sheet_description
			new_item.get_node("Panel/BtnOpen").connect("pressed", Callable(self, "_on_SideBar_Item_open").bind(new_item_data.sheet_id))
	
	
func _save_external_data():
	var res_path = dialog_data.resource_path
	ResourceSaver.save(dialog_data, res_path, 0)

# ==============================================================================
# UI CALLBACKS


## Distributes the input event to the appropriate method
func _on_GraphEdit_gui_input(event: InputEvent) -> void:
	if (event is InputEventMouseButton) and (event.pressed):
		match event.button_index:
			MOUSE_BUTTON_LEFT:
				_on_GraphEdit_left_click(event)
				
			MOUSE_BUTTON_RIGHT:
				_on_GraphEdit_right_click(event)
	#if (event is InputEventMouseMotion):
	#	print( (graph_area.get_local_mouse_position() + graph_area.scroll_offset)/graph_area.zoom )


## Handles left clicks
func _on_GraphEdit_left_click(event: InputEvent) -> void:
	# event.position is screen coordinate, not taking scroll into account
	# graph_position is in node local coordinates
	var graph_position = event.position + graph_area.scroll_offset
	
	#print("LEFT CLICK: " + str(graph_position))
	#print(graph_area.scroll_offset)

## Handles right clicks
func _on_GraphEdit_right_click(event: InputEvent) -> void:
	# event.position is screen coordinate, not taking scroll into account
	# graph_position is in node local coordinates
	var graph_position = event.position + graph_area.scroll_offset

	var cursor_position =  Vector2(get_viewport().get_mouse_position() if get_viewport().gui_embed_subwindows else DisplayServer.mouse_get_position())
	graph_area_popup.popup(Rect2(cursor_position, Vector2(10,10)))


# When a node item (message, condition, effect) is mouse-hovered
# Also happens if dragging started on another object
func _on_item_mouse_entered(obj: Control) -> void:
	hovering_object = obj
	if (dragging_object != null) and (dragging_object != obj):
		obj.modulate.a = 0.7
		obj.dragdrop_line.show()

# When a node item (message, condition, effect) loses mouse hover
# Also happens if dragging started on another object
func _on_item_mouse_exited(obj: Control) -> void:
	if hovering_object == obj:
		hovering_object = null
	if dragging_object != null:
		obj.modulate.a = 1.0
		obj.dragdrop_line.hide()


func _on_sequence_mouse_entered(obj: Control):
	hovering_object = obj
	if dragging_object != null:
		obj.modulate.a = 0.7

func _on_sequence_mouse_exited(obj: Control):
	if hovering_object == obj:
		hovering_object = null
	if dragging_object != null:
		obj.modulate.a = 1.0


# When the mouse is pressed down on a node item, which counts as 
# start dragging it.
func _on_item_drag_started(obj: Control) -> void:
	dragging_object = obj


# When the mouse is released after dragging an item. The obj argument
# contains the object being dragged, not the one under the cursor
func _on_item_drag_ended(obj: Control) -> void:
	if (dragging_object != hovering_object):
		move_item_by_instance(dragging_object, hovering_object)
		
	if hovering_object and is_instance_valid(hovering_object):
		hovering_object.modulate.a = 1.0
		if not hovering_object is DialogGraphNode:
			hovering_object.dragdrop_line.hide()
	
	hovering_object = null
	dragging_object = null


func move_item_by_instance(source_inst: Control, dest_inst):
	if (not source_inst) or (not is_instance_valid(source_inst)):
		return
	if (not dest_inst) or (not is_instance_valid(dest_inst)):
		return
	
	var source_seq = source_inst.sequence_node
	var data_seq_origin = source_seq.data
	var source_index = data_seq_origin.items.find(source_inst.data)
	var source_item_data = data_seq_origin.items[source_index]
	
	var dest_seq
	var data_seq_dest
	var dest_index

	if dest_inst is DialogGraphNode:
		# Dragging onto a sequence header
		dest_seq = dest_inst
		data_seq_dest = dest_seq.data
		dest_index = data_seq_dest.items.size()
	
	else:
		# Dragging onto another item
		dest_seq = dest_inst.sequence_node
		data_seq_dest = dest_seq.data
		dest_index = data_seq_dest.items.find(dest_inst.data)
	
	# There are special cases if the node is being reordered inside the same sequence
	if (data_seq_origin == data_seq_dest):
		if (dest_index == (source_index+1)):
			# If the user dropped on the item immediately below, no operation is needed
			return
		
		elif (dest_index > source_index):
			# If item is being moved below, removing it first will shift indices
			# below that point, causing the reinsert to have an unintended
			# extra offset of 1. So counteract is needed
			dest_index -= 1
	
	data_seq_origin.items.remove_at(source_index)
	data_seq_dest.items.insert(dest_index, source_item_data)
	
	source_seq.update_from_data()
	dest_seq.update_from_data()
	
	await get_tree().create_timer(0.02).timeout
	call_deferred("rebuild_connections")



func _on_GraphArea_connection_request(from, from_slot, to, to_slot):
	# Get the required data
	var from_node = graph_area.get_node(NodePath(from))
	var from_data = from_node.get_data_by_port(from_slot)
	var to_node = graph_area.get_node(NodePath(to))
	# to_slot is always 0 in this application
	var to_sequence_id = to_node.data.sequence_id
	
	# Make the connection in the underlying data resources
	if from_data is DialogNodeData:
		# This is a simple continue
		from_data.continue_sequence_id = to_sequence_id
	
	else:
		# This is a branching
		from_data.connected_to_id = to_sequence_id
	
	rebuild_connections()


func _on_GraphArea_disconnection_request(from, from_slot, to, to_slot):
	# Get the required data
	var from_node = graph_area.get_node(NodePath(from))
	var from_data = from_node.get_data_by_port(from_slot)
	
	# Make the connection in the underlying data resources
	if from_data is DialogNodeData:
		# This is a simple continue
		from_data.continue_sequence_id = -1
	
	else:
		# This is a branching
		from_data.connected_to_id = -1
	
	rebuild_connections()


func _on_node_connections_changed() -> void:
	rebuild_connections()

func _on_node_close_request(node_object) -> void:
	deleting_node = node_object
	popup_delete_node.popup_centered()
	

func _on_SideBar_SearchEdit_text_changed(new_text) -> void:
	update_sidebar()


func _on_GraphArea_PopupMenu_id_pressed(id) -> void:
	match id:
		0:
			# Create new node
			
			# graph_area_popup.rect_position is screen coordinate, not taking scroll into account
			# graph_position is in node local coordinates
			#var cursor_position =  Vector2(graph_area_popup.position)
			#var graph_position = Vector2(cursor_position) + Vector2(graph_area.scroll_offset)
			#var graph_position = Vector2(graph_area_popup.position) + Vector2(graph_area.scroll_offset)
			var graph_position = Vector2((graph_area.get_local_mouse_position() + graph_area.scroll_offset)/graph_area.zoom)

			create_new_node(graph_position - Vector2(100,10), false)
			open_sheet(current_sheet)


func _on_DialogDeleteNodePopup_confirmed() -> void:
	if (not current_sheet) or (not deleting_node):
		return
	
	var sheet_data = dialog_data.sheets[current_sheet]
	var node_data = deleting_node.data
	
	sheet_data.nodes.erase(node_data)
	
	# Reopens the sheet to update area
	open_sheet(current_sheet)


func _on_BtnEditSheet_pressed() -> void:
	if not current_sheet:
		return
		
	var sheet_data = dialog_data.sheets[current_sheet]
	dialog_sheet_edit.open(sheet_data)
	
func _on_DialogSheetEdit_sheet_saved(sheet_id, sheet_desc, delete_word) -> void:
	if not current_sheet:
		return

	# Is the user requesting to delete the sheet?
	if delete_word == "delete":
		dialog_data.sheets[current_sheet].nodes = [] # Discards references to node data
		dialog_data.sheets[current_sheet] = null
		dialog_data.sheets.erase(current_sheet)
		
		# If this was the last sheet, we create a new one
		if dialog_data.sheets.size() == 0:
			# This method creates a new sheet, sets as current and returns the 
			# new sheet name
			var _new_sheet = create_new_sheet()

		# Otherwise select some other sheet
		else:
			current_sheet = dialog_data.sheets.keys()[0]
			open_sheet(current_sheet)
		
		# Hide the window
		dialog_sheet_edit.hide()
		
		update_sidebar()
		# Stop here since the old sheet is no longer valid
		return

	# Otherwise the user is editting the sheet
	
	var sheet_data = dialog_data.sheets[current_sheet]

	# Check if the sheet is being renamed and if the name does not collide
	if sheet_id != sheet_data.sheet_id:
		# User wants to rename
		# Check if this id is invalid or being used
		if (sheet_id == "") or (sheet_id in dialog_data.sheets):
			# Show an error message instead
			dialog_sheet_rename_error_popup.popup_centered()
			# Stop here and don't make any changes
			return
			
		else:
			# Rename the sheet
			dialog_data.sheets.erase(current_sheet)
			sheet_data.sheet_id = sheet_id
			dialog_data.sheets[sheet_id] = sheet_data
			current_sheet = sheet_id
	
	# Change description
	sheet_data.sheet_description = sheet_desc
	
	# Editting sheet details (ID, description) does not change node content
	# so calling open_sheet() is not required
	
	# Update current sheet panel and listing
	update_sidebar()
	# Hide window
	dialog_sheet_edit.hide()
	
	#var mtdefs = MTDefs.new()
	#mtdefs.debug_resource(dialog_data)

func _on_SideBar_Item_open(sheet_id) -> void:
	open_sheet(sheet_id)

func _on_BtnNewSheet_pressed() -> void:
	create_new_sheet()
	
	# Open sheet edit window
	_on_BtnEditSheet_pressed()
	
	# Inform user about successful sheet creation and edit window
	dialog_sheet_create_popup.popup_centered()


func _on_BtnSaveDB_pressed():
	var res_path = dialog_data.resource_path
	ResourceSaver.save(dialog_data, res_path, 0)


func _on_ImportExport_BtnExport_pressed() -> void:
	dialog_export.set_current_sheet(current_sheet, true)
	dialog_export.refresh_export_sheet_list()
	dialog_export.popup_centered()


func _on_ImportExport_BtnImportSheet_pressed() -> void:
	dialog_import.set_current_sheet(current_sheet)
	dialog_import.reset_and_show()


func _on_dialog_import_import_executed(destination_sheet: String) -> void:
	if destination_sheet != "":
		open_sheet(destination_sheet)
	else:
		reopen_current_sheet()
